<?php
defined('_JEXEC') or die('Restricted access');

/**
 * A class to hold a collection of objects of  the same type.
 *  found at: http://codereview.stackexchange.com/questions/13290/php-class-to-represent-a-type-collection
 * There are tow purposes:
 * - to ensure that all elements are of the same type
 * - to be enumerable by the foreach loop
 */

abstract class TypedCollection implements Countable, Iterator, ArrayAccess{
    
    /**
     * The array that contains collection elements
     * @var array 
     */

    protected $_innerArray = array();

    /**
     * The collection type
     * @var ReflectionClass
     */

    protected $_type;
    
    
    
    /**
     * Child classes pass the collection type to this constructor
     * 
     * @param string $typeName The name of the underlying class
     * @throws InvalidArgumentException If the class can't be found by reflection
     */
    public function __construct($typeName){
        if(class_exists($typeName)){
            $this->_type = new ReflectionClass($typeName);
        } else {
            throw new InvalidArgumentException("The class $typeName does not exist.");
        }
    }

    /* Implement Countable */

    /**
     * Returns the count of the elements in the collection
     * @return int 
     */
    public function count(){ return count($this->_innerArray); }

    /* Implement Iterator */

    /**
     * Returns the current element of the collection
     * @return Object 
     */
    public function current(){ return current($this->_innerArray); }

    /**
     * Return the key of the current element
     * @return int 
     */
    public function key(){ return key($this->_innerArray); }

    /**
     * Move forward to next element
     */
    public function next(){ next($this->_innerArray); }

    /**
     * Rewind to its first element 
     */
    public function rewind(){ reset($this->_innerArray); }

    /**
     * Checks if the current position is valid
     * @return bool 
     */
    public function valid(){ return $this->current() !== false; }

    /* Implement ArrayAccess */

    /**
     * Whether an offset exists
     * @param int $offset
     * @return bool 
     */
    public function offsetExists($offset){ return isset($this->_innerArray[$offset]); }

    /**
     * Returns the value at the specified offset
     * @param int $offset
     * @return int 
     */
    public function offsetGet($offset){
        if(isset($this->_innerArray[$offset])){
            return $this->_innerArray[$offset];
        } else {
            return null;
        }
    }

    /**
     * Assigns a value to the specified offset
     * @param int $offset 
     * @param mixed $object 
     * @throws InvalidArgumentException If $object is not of the underlying type
     */
    public function offsetSet($offset, $object){
        if ($object instanceof $this->_type->name){
            if (is_null($offset)){ 
                $this->_innerArray[] = $object;
            } else {
                $this->_innerArray[$offset] = $object;
            }
        } else {
            throw new InvalidArgumentException("Object needs to be a {$this->_type->name} instance.");
        }
    }

    /**
     * Unsets an offset
     * @param int $offset 
     */
    public function offsetUnset($offset){
        unset($this->_innerArray[$offset]);
        $this->_innerArray = array_values($this->_innerArray);
    }

    /* TypedCollection functions */

    /**
     * Adds an object to the collection.
     * @param mixed $object The object to add to the collection
     */
    public function add($object){ $this->offsetSet(null, $object); }

    /**
     * Removes all elements from the collection 
     */
    public function clear(){ $this->_innerArray = array(); }

    /**
     * Checks if an object belongs to the collection
     * @param mixed $object
     * @return bool 
     */
    public function contains($object){ return in_array($object, $this->_innerArray, true); }

    /**
     * Return an object index
     * @param mixed $object
     * @return int 
     */
    public function indexOf($object){ return array_search($object); }

    /**
     * Inserts an object at the specified offset in the collection
     * @param int $offset
     * @param mixed $object
     * @throws InvalidArgumentException If the object is not of the underlying type
     * @throws OutOfRangeException If the offset does not exist
     */
    public function insert($offset, $object){
        if(array_key_exists($offset, $this->_innerArray)){
            if($object instanceof $this->_type->name){
                $tempArray = array($object, $this->offsetGet($offset));
                array_splice($this->_innerArray, $offset, 1, $tempArray);
                $this->_innerArray = array_values($this->_innerArray);
            } else { throw new InvalidArgumentException("Object needs to be a {$this->_type->name} instance."); }
        } else {     throw new OutOfRangeException("The index $offset does not exist in the collection."); }
    }

    /**
     * Removes the specified object from the collection
     * @param mixed $object 
     */
    public function remove($object){
        if($this->contains($object)){
            $this->offsetUnset(array_search($object, $this->_innerArray));
            $this->_innerArray = array_values($this->_innerArray);
        }
    }    

    /**
     * Removes the object at the specified offset in the collection
     * @param int $offset 
     */
    public function removeAt($offset){
        $this->offsetUnset($offset);
        $this->_innerArray = array_values($this->_innerArray);
    }

}
?>
